/*
 *  Copyright (c) 2005-2009, WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
 *
 *  WSO2 Inc. licenses this file to you under the Apache License,
 *  Version 2.0 (the "License"); you may not use this file except
 *  in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing,
 *  software distributed under the License is distributed on an
 *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 *  KIND, either express or implied.  See the License for the
 *  specific language governing permissions and limitations
 *  under the License.
 *
 */
package org.wso2.carbon.registry.core.jdbc.dao;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.wso2.carbon.registry.core.Comment;
import org.wso2.carbon.registry.core.RegistryConstants;
import org.wso2.carbon.registry.core.ResourceIDImpl;
import org.wso2.carbon.registry.core.ResourceImpl;
import org.wso2.carbon.registry.core.dao.CommentsDAO;
import org.wso2.carbon.registry.core.dao.ResourceDAO;
import org.wso2.carbon.registry.core.dataaccess.DAOManager;
import org.wso2.carbon.registry.core.exceptions.RegistryException;
import org.wso2.carbon.registry.core.jdbc.DatabaseConstants;
import org.wso2.carbon.registry.core.jdbc.dataaccess.JDBCDatabaseTransaction;
import org.wso2.carbon.registry.core.jdbc.dataobjects.CommentDO;
import org.wso2.carbon.registry.core.session.CurrentSession;
import org.wso2.carbon.registry.core.utils.RegistryUtils;
import org.wso2.carbon.utils.DBUtils;

import java.sql.*;
import java.util.ArrayList;
import java.util.List;

/**
 * An extension of {@link JDBCCommentsVersionDAO} implements {@link JDBCCommentsDAO} to store
 * comments on a JDBC-based database, when versioning for comments has been enabled.
 */
public class JDBCCommentsVersionDAO extends JDBCCommentsDAO implements CommentsDAO {

    private static final Log log = LogFactory.getLog(JDBCCommentsVersionDAO.class);
    private ResourceDAO resourceDAO;

    /**
     * Default constructor
     *
     * @param daoManager instance of the data access object manager.
     */
    public JDBCCommentsVersionDAO(DAOManager daoManager) {
        super(daoManager);
        this.resourceDAO = daoManager.getResourceDAO();
    }

    /**
     * Method to persist a comment.
     *
     * @param resource the resource
     * @param userID   the id of the user who added the comment.
     * @param comment  the comment to be persisted.
     *
     * @return the comment id of the newly added comment.
     * @throws RegistryException if some error occurs while adding a comment
     */
    public int addComment(ResourceImpl resource, String userID, Comment comment)
            throws RegistryException {
        JDBCDatabaseTransaction.ManagedRegistryConnection conn =
                JDBCDatabaseTransaction.getConnection();
        PreparedStatement ps1 = null, ps2 = null, ps3 = null;
        int commentId = -1;
        try {
            String sql1 = "INSERT INTO REG_COMMENT (REG_COMMENT_TEXT," +
                    "REG_USER_ID, REG_COMMENTED_TIME, REG_TENANT_ID) VALUES (?, ?, ?, ?)";
            String sql2 = "SELECT MAX(REG_ID) FROM REG_COMMENT";
            String sql3 = "INSERT INTO REG_RESOURCE_COMMENT (REG_COMMENT_ID, " +
                    "REG_VERSION, REG_TENANT_ID) VALUES (?, ?, ?)";

            String dbProductName = conn.getMetaData().getDatabaseProductName();
            boolean returnsGeneratedKeys = DBUtils.canReturnGeneratedKeys(dbProductName);
            if (returnsGeneratedKeys) {
                ps1 = conn.prepareStatement(sql1, new String[]{DBUtils
                        .getConvertedAutoGeneratedColumnName(dbProductName,
                                DatabaseConstants.ID_FIELD)});
            } else {
                ps1 = conn.prepareStatement(sql1);
            }
            ps3 = conn.prepareStatement(sql3);

            // prepare to execute query2 for the comments
            ps1.setString(1, comment.getText());
            ps1.setString(2, userID);

            long now = System.currentTimeMillis();
            ps1.setTimestamp(3, new Timestamp(now));
            ps1.setInt(4, CurrentSession.getTenantId());
            ResultSet resultSet1;
            if (returnsGeneratedKeys) {
                ps1.executeUpdate();
                resultSet1 = ps1.getGeneratedKeys();
            } else {
                synchronized (ADD_COMMENT_LOCK) {
                    ps1.executeUpdate();
                    ps2 = conn.prepareStatement(sql2);
                    resultSet1 = ps2.executeQuery();
                }
            }
            try {
                if (resultSet1.next()) {
                    // setting the RES_COMMENTS_ID
                    commentId = resultSet1.getInt(1);

                    ps3.setInt(1, commentId);
                    ps3.setLong(2, resource.getVersionNumber());
                    ps3.setInt(3, CurrentSession.getTenantId());
                    ps3.executeUpdate();
                }
            } finally {
                if (resultSet1 != null) {
                    resultSet1.close();
                }
            }

        } catch (SQLException e) {

            String msg = "Failed to add comments to the resource " +
                    resource.getPath() + ". " + e.getMessage();
            log.error(msg, e);
            throw new RegistryException(msg, e);
        } finally {
            try {
                try {
                    if (ps1 != null) {
                        ps1.close();
                    }
                } finally {
                    try {
                        if (ps2 != null) {
                            ps2.close();
                        }
                    } finally {
                        if (ps3 != null) {
                            ps3.close();
                        }
                    }
                }
            } catch (SQLException ex) {
                String msg = RegistryConstants.RESULT_SET_PREPARED_STATEMENT_CLOSE_ERROR;
                log.error(msg, ex);
            }
        }
        return commentId;
    }

    /**
     * Method to copy comments.
     *
     * @param sourceResource the source resource.
     * @param targetResource the target resource.
     *
     * @throws RegistryException if some error occurs while copying comments
     */
    public void copyComments(ResourceImpl sourceResource, ResourceImpl targetResource)
            throws RegistryException {

        Comment[] comments = getComments(sourceResource);
        CommentDO[] commentDOs = new CommentDO[comments.length];
        for (int i = 0; i < comments.length; i++) {
            CommentDO commentDO = new CommentDO();
            commentDO.setCommentedUser(comments[i].getUser());
            commentDO.setCommentText(comments[i].getText());
            commentDOs[i] = commentDO;
        }
        addComments(targetResource, commentDOs);
    }

    /**
     * Method to persist comments.
     *
     * @param resource   the resource
     * @param commentDOs the comments to be persisted.
     *
     * @throws RegistryException if some error occurs while adding comments
     */
    public void addComments(ResourceImpl resource, CommentDO[] commentDOs)
            throws RegistryException {
        JDBCDatabaseTransaction.ManagedRegistryConnection conn =
                JDBCDatabaseTransaction.getConnection();
        PreparedStatement ps1 = null, ps2 = null, ps3 = null;
        try {
            String sql1 = "INSERT INTO REG_COMMENT (REG_COMMENT_TEXT," +
                    "REG_USER_ID, REG_COMMENTED_TIME, REG_TENANT_ID) VALUES (?, ?, ?, ?)";
            String sql2 = "SELECT MAX(REG_ID) FROM REG_COMMENT";
            String sql3 = "INSERT INTO REG_RESOURCE_COMMENT (REG_COMMENT_ID, " +
                    "REG_VERSION, REG_TENANT_ID) VALUES (?, ?, ?)";

            String dbProductName = conn.getMetaData().getDatabaseProductName();
            boolean returnsGeneratedKeys = DBUtils.canReturnGeneratedKeys(dbProductName);
            if (returnsGeneratedKeys) {
                ps1 = conn.prepareStatement(sql1, new String[]{DBUtils
                        .getConvertedAutoGeneratedColumnName(dbProductName,
                                DatabaseConstants.ID_FIELD)});
            } else {
                ps1 = conn.prepareStatement(sql1);
            }
            ps3 = conn.prepareStatement(sql3);

            for (CommentDO comment : commentDOs) {
                // prepare to execute query2 for the comments
                ps1.setString(1, comment.getCommentText());
                ps1.setString(2, comment.getCommentedUser());

                long now = System.currentTimeMillis();
                ps1.setTimestamp(3, new Timestamp(now));
                ps1.setInt(4, CurrentSession.getTenantId());
                ResultSet resultSet1;
                if (returnsGeneratedKeys) {
                    ps1.executeUpdate();
                    resultSet1 = ps1.getGeneratedKeys();
                } else {
                    synchronized (ADD_COMMENT_LOCK) {
                        ps1.executeUpdate();
                        ps2 = conn.prepareStatement(sql2);
                        resultSet1 = ps2.executeQuery();
                    }
                }
                try {
                    if (resultSet1.next()) {
                        // setting the RES_COMMENTS_ID
                        int commentId = resultSet1.getInt(1);

                        ps3.setInt(1, commentId);
                        ps3.setLong(2, resource.getVersionNumber());
                        ps3.setInt(3, CurrentSession.getTenantId());

                        ps3.executeUpdate();
                        ps3.clearParameters();
                    }
                } finally {
                    if (resultSet1 != null) {
                        resultSet1.close();
                    }
                }
                ps3.clearParameters();
            }

        } catch (SQLException e) {

            String msg = "Failed to add comments to the resource " +
                    resource.getPath() + ". " + e.getMessage();
            log.error(msg, e);
            throw new RegistryException(msg, e);
        } finally {
            try {
                try {
                    if (ps1 != null) {
                        ps1.close();
                    }
                } finally {
                    try {
                        if (ps2 != null) {
                            ps2.close();
                        }
                    } finally {
                        if (ps3 != null) {
                            ps3.close();
                        }
                    }
                }
            } catch (SQLException ex) {
                String msg = RegistryConstants.RESULT_SET_PREPARED_STATEMENT_CLOSE_ERROR;
                log.error(msg, ex);
            }
        }
    }

    /**
     * Method to get comments added to a given resource.
     *
     * @param resource the resource.
     *
     * @return an array of comments.
     * @throws RegistryException if an error occurs while getting comments.
     */
    public Comment[] getComments(ResourceImpl resource) throws RegistryException {

        JDBCDatabaseTransaction.ManagedRegistryConnection conn =
                JDBCDatabaseTransaction.getConnection();

        List<Comment> commentList = new ArrayList<Comment>();
        PreparedStatement s = null;
        ResultSet results = null;
        String path = resource.getPath();
        try {
            String sql =
                    "SELECT C.REG_ID, C.REG_COMMENT_TEXT, C.REG_USER_ID, C.REG_COMMENTED_TIME " +
                            "FROM REG_COMMENT C, REG_RESOURCE_COMMENT RC " +
                            "WHERE C.REG_ID=RC.REG_COMMENT_ID AND RC.REG_VERSION = ? " +
                            "AND C.REG_TENANT_ID=? AND RC.REG_TENANT_ID=?";
            s = conn.prepareStatement(sql);
            s.setLong(1, resource.getVersionNumber());
            s.setInt(2, CurrentSession.getTenantId());
            s.setInt(3, CurrentSession.getTenantId());

            results = s.executeQuery();
            while (results.next()) {
                Comment comment = new Comment();
                comment.setText(results.getString(DatabaseConstants.COMMENT_TEXT_FIELD));
                comment.setUser(results.getString(DatabaseConstants.USER_ID_FIELD));
                comment.setCreatedTime(
                        results.getTimestamp(DatabaseConstants.COMMENTED_TIME_FIELD));
                comment.setResourcePath(path);
                String commentPath = path + RegistryConstants.URL_SEPARATOR + "comments:" +
                        results.getInt(DatabaseConstants.ID_FIELD);
                comment.setPath(commentPath);
                comment.setCommentPath(commentPath);
                comment.setParentPath(path + RegistryConstants.URL_SEPARATOR + "comments");
                comment.setCommentID(results.getLong(DatabaseConstants.ID_FIELD));
                commentList.add(comment);
            }

        } catch (SQLException e) {

            String msg = "Failed to get comments on resource " + path + ". " + e.getMessage();
            log.error(msg, e);
            throw new RegistryException(msg, e);
        } finally {
            try {
                try {
                    if (results != null) {
                        results.close();
                    }
                } finally {
                    if (s != null) {
                        s.close();
                    }
                }
            } catch (SQLException ex) {
                String msg = RegistryConstants.RESULT_SET_PREPARED_STATEMENT_CLOSE_ERROR;
                log.error(msg, ex);
            }
        }

        return commentList.toArray(new Comment[commentList.size()]);
    }

    /**
     * Method to get resource paths of comments.
     *
     * @param commentIDs the comment id.
     * @param conn       the connection to use.
     *
     * @return array of resource paths.
     * @throws RegistryException if an error occurs.
     */
    public String[] getResourcePathsOfComments(Long[] commentIDs, Connection conn)
            throws RegistryException {

        if (commentIDs.length == 0) {
            return new String[0];
        }
        StringBuffer sqlBuf = new StringBuffer();
        sqlBuf.append("SELECT DISTINCT RC.REG_COMMENT_ID, RC.REG_VERSION FROM " +
                "REG_RESOURCE_COMMENT RC WHERE (");
        for (int i = 0; i < commentIDs.length; i++) {
            if (i > 0) {
                sqlBuf.append(" OR ");
            }
            sqlBuf.append("RC.REG_COMMENT_ID=?");
        }
        sqlBuf.append(") AND RC.REG_TENANT_ID=?");

        List<String> commentPathList = new ArrayList<String>();

        ResultSet results = null;
        PreparedStatement s = null;
        try {
            s = conn.prepareStatement(sqlBuf.toString());
            int i;
            for (i = 0; i < commentIDs.length; i++) {
                s.setLong(i + 1, commentIDs[i]);
            }

            s.setInt(i + 1, CurrentSession.getTenantId());

            results = s.executeQuery();
            while (results.next()) {
                long commentID = results.getLong(DatabaseConstants.COMMENT_ID_FIELD);
                long version = results.getLong(DatabaseConstants.VERSION_FIELD);
                if (version > 0) {
                    String path = resourceDAO.getPath(version);
                    if (path != null) {
                        String commentPath =
                                path + RegistryConstants.URL_SEPARATOR + "comments:" + commentID;
                        commentPathList.add(commentPath);
                    }
                }
            }

        } catch (SQLException e) {


            String msg = "Failed to get the resource for the set of comment ids."
                    + e.getMessage();
            log.error(msg, e);
            throw new RegistryException(msg, e);
        } finally {
            try {
                try {
                    if (results != null) {
                        results.close();
                    }
                } finally {
                    if (s != null) {
                        s.close();
                    }
                }
            } catch (SQLException ex) {
                String msg = RegistryConstants.RESULT_SET_PREPARED_STATEMENT_CLOSE_ERROR;
                log.error(msg, ex);
            }
        }

        return commentPathList.toArray(new String[commentPathList.size()]);
    }

    /**
     * Gets the resource with sufficient data to differentiate it from another resource. This would
     * populate a {@link ResourceImpl} with the <b>path</b>, <b>name</b> and <b>path identifier</b>
     * of a resource.
     *
     * @param path the path of the resource.
     *
     * @return the resource with minimum data.
     * @throws RegistryException if an error occurs while retrieving resource data.
     */
    public ResourceImpl getResourceWithMinimumData(String path) throws RegistryException {
        return RegistryUtils.getResourceWithMinimumData(path, resourceDAO, true);
    }

    /**
     * Method to move comments. This function is not applicable to versioned resources.
     *
     * @param source the source resource.
     * @param target the target resource.
     *
     * @throws RegistryException if some error occurs while moving comments
     */
    public void moveComments(ResourceIDImpl source, ResourceIDImpl target)
            throws RegistryException {
        // this is non-versioned specific function.
        // do nothing when the comments versioning on in configuration
    }

    /**
     * Method to move comment paths. This function is not applicable to versioned resources.
     *
     * @param source the source resource.
     * @param target the target resource.
     *
     * @throws RegistryException if some error occurs while moving comment paths
     */
    public void moveCommentPaths(ResourceIDImpl source, ResourceIDImpl target)
            throws RegistryException {
        // this is non-versioned specific function.
        // do nothing when the comments versioning on in configuration
    }

}
